// 参考文章：https://segmentfault.com/a/1190000006599500#articleHeader3

let target = null;

/***
 *
 * @constructor
 * 名称：Vue构造函数
 */
function Vue(options = {}) {
    this.$options = options;
    this._data = this.$options.data;
    // 1、给实例this添加属性，这些新添加的属性值就是this._data[key]的值。当改变vm[key]的值时，this._data[key]值也会改变
    for (const key in this._data) {
        Object.defineProperty(this, key, {
            enumerable: true,
            get() {
                return this._data[key];
            },
            set(newVal) {
                this._data[key] = newVal;
            }
        });
    }

    // 2、实例化一个Observer对象，来观测this._data对象里的属性
    // 这一步必须要在Compile之前，因为监听了属性后，在new Compile中第一次get值时，使用Dep.target来建立【Observer-Compile】的联系
    new Observer(this._data);

    // 3、实例化一个Compile对象
    new Compile(this.$options.el, this);
}


/**
 *
 * @constructor
 * 名称：消息订阅器构造函数，这里的消息主要就是属性，也称属性订阅器
 * 作用：1、维护一个数组，用来收集订阅者。
 * 2、当数据变动时，监听器Observer调用消息订阅器Dep方法通知消息订阅器，消息订阅器调用订阅者Watcher方法
 */
function Dep() {
    this.subs = []; // 订阅者Watcher实例对象的数组
}
Dep.prototype.addSub = function(sub) {
    this.subs.push(sub);
};
Dep.prototype.notify = function() { // notify方法遍历subs数组并调用每个子元素sub的update()方法。
    // this.subs.forEach(sub => sub.update());
    // 这样遍历的话，会把data里的所有数据都更新
    this.subs.forEach(function(sub) {
        sub.update();
    });
};


/**
 *
 * @param vm  对象
 * @param key  对象的键名
 * @param fn  怎么处理vm[key]的值，vm[key]是新值。怎么将vm[key]处理后显示出来，即是实现怎么更新数据的函数，这个由Vue编译时，具体实现。即是Compile中的一个回调函数。
 * @param str
 * @constructor
 * 名称：订阅者构造函数
 * 作用：消息订阅器Dep调用订阅者Watcher方法，来通知订阅者更新数据
 * 哪些会创建订阅者：因为涉及到怎么在页面显示更新后的数据，所以该对象是和页面DOM有关的。在页面上使用到的变量，才会创建Watcher实例对象。
 * 实例化几次：一个变量就需创建一个订阅者，所以页面上会存在多个订阅者。
 */
function Watcher(vm, key, fn, str) {
    this.vm = vm;
    this.key = key;
    this.fn = fn;
    this.str = str;

    // 注：下面两种写法实现的功能都是一致的：因为需要一个全局的变量来作为中间值将【Observer-Compile】建立联系，所以将Watcher实例对象赋值给一个全局变量。让两个构造函数都能获取到这个中间值。
    // target = this;
    // Dep.target = this; // 将Watcher实例赋值给Dep.target。target属性是Dep的一个静态属性
}

Watcher.prototype.update = function() {
    this.fn(this.vm[this.key]); // 用vm[key]新值替换正则匹配内容。替换时又会get。这时Dep.target为null。
};


/**
 *
 * @param el
 * @param vm
 * @constructor
 * 名称：编译HTML构造函数
 * 作用：1、将【Observer-Compile】。2、建立联系使用js变量替换{{}}模板的值
 */
// 编译HTML：使用js变量替换{{}}模板的值
function Compile(el, vm) {
    vm.$el = document.querySelector(el);
    const fragment = document.createDocumentFragment();
    let child = null;
    while (child = vm.$el.firstChild) { // 执行child = vm.$el.firstChild赋值语句，该语句返回值是vm.$el.firstChild，为true
        fragment.append(child); // 会从vm.$el里将child移走，并添加到fragment对象里。append一个已经存在于文档中的dom结构，相当于将它从原位置移动到新位置。这样vm.$el每次就会少一个子节点。
    }
    replace(fragment);
    vm.$el.append(fragment); // 模板替换完毕，将fragment插入到已经空了的vm.$el中。至此，模板编译完成。

    function replace(fragment) {
        const pattern = /\{\{(.*)\}\}/; // {{str}}正则表达式
        Array.from(fragment.childNodes).forEach(node => {
            let text = node.textContent; // textContent会获取node节点下所有文本节点的文本。（包含本身和子节点的）
            if (node.nodeType === 3 && pattern.test(text)) { // node是文本节点且文本节点的文本内容满足正则
                const key = RegExp.$1.trim(); // RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串，RegExp.$2，RegExp.$3，..RegExp.$99总共可以有99个匹配

                // 1、实例化Watcher构造函数，得到一个Watcher实例对象。
                // 这个实例对象其实是【node节点-vue变量】构成的唯一的Watcher。即如果vue变量在不同的node下出现3次，那么就会生成三个watcher。
                let w = new Watcher(vm, key, (newVal) => {
                    // 这个node文本对象一直被Watcher引用，一直存在在内存中。垃圾回收时会忽略有引用的对象。
                    node.textContent = text.replace(pattern, newVal); // 正则匹配到的内容用对应的vm[key]替换
                }, 'test');

                // 2、下面这代码可以写在Watcher构造函数内部，也可以在这里写。
                // 注：下面两种写法实现的功能都是一致的：因为需要一个全局的变量来作为中间值将【Observer-Compile】建立联系，所以将Watcher实例对象赋值给一个全局变量。让两个构造函数都能获取到这个中间值。
                target = w;
                Dep.target = w;

                // 3、取值vm[key]相当于取值this._data[key]，会触发Observer实例监听变量的get。这次get会读取中间值target的值并将值加入到Dep实例中
                node.textContent = text.replace(pattern, vm[key]);
            }
            if (node.childNodes && node.childNodes.length) { // 如果node节点下还有节点，则继续向下遍历进行替换。文本节点的childNodes是个空的NodeList集合。
                replace(node);
            }
        })
    }
}


/**
 *
 * @param data 数据对象
 * @returns {*}
 * @constructor
 * 名称：监听器构造函数
 * 作用：1、将对象的所有属性进行数据劫持，便于当对象属性值发生变化时，能监听到
 * 2、当监听到对象属性发生变化时，调用消息订阅器Dep的方法通知订阅器，数据发生了变化
 *
 */
// 对数据对象Model的所有属性进行监听，当Model对象改变时，将Model的改变同步到View即同步到HTML页面
function Observer(data) {
    const dep = new Dep();
    for (const key in data) {
        let val = data[key];
        Object.defineProperty(data, key, {
            enumerable: true,
            get() { // 取值
                // 1、这一步非常重要：读取全局变量值中间值，将Watcher订阅者加入到Dep集合中，将【Observer-Compile】建立联系
                // 如果当前的属性虽然使用了，但没有创建订阅者实例，即没有new Watcher，那么Dep.target就是undefined。就没有该属性的消息订阅器。
                // 如果当前属性创建了订阅者Watcher实例后，没有马上对该属性进行取值，那么可能后面的Dep.target指向的Watcher实例并不是该属性的了。
                // 所以需保证：创建了该属性的Watcher实例后，这时Dep.target指向的就是该属性的Watcher，必须马上对该属性进行取值操作来强制触发该属性的getter方法，才能正确的将该属性的Watcher实例添加进消息订阅器里。
                target && dep.addSub(target);
                target = null; // 添加后将静态属性target置为null。释放Dep.target对Watcher实例的一个引用。
                // Dep.target && dep.addSub(Dep.target);
                // Dep.target = null; // 添加后将静态属性target置为null。释放Dep.target对Watcher实例的一个引用。

                // 2、返回值
                return val;
            },
            set(newVal) { // 赋值
                if (newVal === val) {
                    return;
                }
                val = newVal;
                dep.notify();
            }
        });
    }
}



/*
当第一次实例化Vue后：
1、得到一个Observer实例。
2、实例化Observer，得到一个Dep实例。只是实例化一个Dep，没对Dep实例进行任何操作。
3、对data对象里的所有属性都进行了get取值监听和set赋值监听。

当第一次编译HTML模板后：
1、得到多个Watcher实例。
2、第一编译时会进行替换值，相当于对data[key]取值get。这时会将data[key]的Watcher实例放进Dep实例中。


当对vm.name赋值set时：
1、会对this._data['name']进行set，set会调用Dep实例的notify方法。notify方法会调用Dep实例里的subs数组里的Watcher实例的update方法


该代码出现的问题：
1、Compile函数：
（1）当某个属性如name在html里出现3次，compile会生成name的3个Watcher实例。
（2）当某个属性如age在data定义了，但在html里没有使用。compile后不会生成age的Watcher实例。
当vm.age时，set调用Dep实例的notify方法。这时Dep实例dep的subs属性数组里没有age的Watcher。html更新不了age。


构造函数功能：
1、Observe：监听data对象里的所有属性的get和set。
2、Dep：执行Watcher里更新属性的方法。
3、Watcher：每个Html文档里出现的data对象里的属性都有一个Watcher实例。Watcher有更新相应属性的方法。

目的：data里name属性初始值为"初始"，显示在<h1>{{name}}<h1>里。执行vm.name = "KUN"后，显示的name值为"KUN"。
方法一：问题：一旦值改变就会重新编译整个片段，太浪费资源和影响效率。应该是当值改变时，只改变html元素的值，不对html元素进行任何移动和创建。
初始化：
1、对data对象的name属性进行监听，Object.defineProperty()
2、编译模板要进行字符串替换：肯定是要先取值vm.name，取值会触发name属性的get监听
赋值：
1、赋值vm.name = "KUN"，会触发name属性的set监听。在set监听里可以重新编译整个作用域的片段。这样也可以实现响应。

方法二：
解决方案：确定HTML将模板定义好了以后，HTML结构不会进行改变。改变的只是显示的数据。
1、在初始化编译HTML时，将替换相应文本节点值的代码复用。因为这些地方使用属性是固定的，不会变化。
2、赋值name时，在name属性的set里调用 使用该name属性的文本节点的替换模板的代码。该代码是全部属性一起替换。因为一个属性可能在多处被用到。
*/
